ReceiptParser.handleVerifiedSequence   A
last analyzed

Complexity

Conditions 1

Size

Total Lines 7
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 7
dl 0
loc 7
ccs 4
cts 4
cp 1
crap 1
rs 10
c 0
b 0
f 0
1 1
import * as ASN1 from 'asn1js'
2
3 1
import {
4
  CONTENT_ID,
5
  FIELD_TYPE_ID,
6
  FIELD_VALUE_ID,
7
  IN_APP,
8
  RECEIPT_FIELDS_MAP,
9
  ReceiptFieldsKeyNames, ReceiptFieldsKeyValues,
10
} from './constants'
11
12 1
import { ReceiptVerifier } from './ReceiptVerifier'
13
14
export type Environment = 'Production' | 'ProductionSandbox' | string
15
16
export type ParsedReceipt = Partial<Record<ReceiptFieldsKeyNames, string>> & {
17
  ENVIRONMENT: Environment
18
  IN_APP_ORIGINAL_TRANSACTION_IDS: string[]
19
  IN_APP_TRANSACTION_IDS: string[]
20
}
21
22
class ReceiptParser {
23
  private readonly parsed: ParsedReceipt
24
  private readonly receiptVerifier: ReceiptVerifier
25
26
  constructor() {
27 5
    this.receiptVerifier = new ReceiptVerifier()
28 5
    this.parsed = this.createInitialParsedReceipt()
29
  }
30
31
  public parseReceipt(receipt: string): ParsedReceipt {
32 5
    if (receipt.trim() === '') {
33 1
      throw new Error('Receipt must be a non-empty string.')
34
    }
35
36 4
    const rootSchemaVerification = this.receiptVerifier.verifyReceiptSchema(receipt)
37 3
    const content = rootSchemaVerification.result[CONTENT_ID] as ASN1.OctetString
38
39 3
    this.parseReceiptContent(content)
40 3
    this.validateParsedFields()
41 3
    this.deduplicateArrayFields()
42
43 3
    return this.parsed
44
  }
45
46
  private createInitialParsedReceipt(): ParsedReceipt {
47 5
    return {
48
      ENVIRONMENT: 'Production',
49
      IN_APP_ORIGINAL_TRANSACTION_IDS: [],
50
      IN_APP_TRANSACTION_IDS: [],
51
    }
52
  }
53
54
  private parseReceiptContent(content: ASN1.OctetString): void {
55 18
    const sequences = this.extractSequencesFromContent(content)
56 18
    sequences.forEach(this.processSequence.bind(this))
57
  }
58
59
  private extractSequencesFromContent(content: ASN1.OctetString): ASN1.Sequence[] {
60 18
    const [contentSet] = content.valueBlock.value as ASN1.Set[]
61 18
    return contentSet.valueBlock.value
62 348
      .filter(v => v instanceof ASN1.Sequence) as ASN1.Sequence[]
63
  }
64
65
  private processSequence(sequence: ASN1.Sequence): void {
66 348
    const verifiedSequence = this.receiptVerifier.verifyFieldSchema(sequence)
67 348
    if (verifiedSequence) {
68 348
      this.handleVerifiedSequence(verifiedSequence)
69
    }
70
  }
71
72
  private handleVerifiedSequence(verifiedSequence: ASN1.CompareSchemaSuccess): void {
73 348
    const fieldKey = (verifiedSequence.result[FIELD_TYPE_ID] as ASN1.Integer).valueBlock.valueDec
74 348
    const fieldValue = verifiedSequence.result[FIELD_VALUE_ID] as ASN1.OctetString
75
76 348
    const handler = this.getFieldHandler(fieldKey)
77 348
    handler(fieldValue)
78
  }
79
80
  private getFieldHandler(fieldKey: number): (fieldValue: ASN1.OctetString) => void {
81 348
    if (fieldKey === IN_APP) {
82 15
      return this.parseReceiptContent.bind(this)
83
    }
84 333
    if (this.isValidReceiptFieldKey(fieldKey)) {
85 159
      const name = RECEIPT_FIELDS_MAP.get(fieldKey)!
86 159
      return (fieldValue: ASN1.OctetString) => {
87 159
        this.addFieldToReceipt(name, this.extractStringValue(fieldValue))
88
      }
89
    }
90 174
    return () => {}
91
  }
92
93
  private isValidReceiptFieldKey(value: unknown): value is ReceiptFieldsKeyValues {
94 333
    return typeof value === 'number' && RECEIPT_FIELDS_MAP.has(value as ReceiptFieldsKeyValues)
95
  }
96
97
  private extractStringValue(field: ASN1.OctetString): string {
98 159
    const [fieldValue] = field.valueBlock.value
99
100 159
    if (fieldValue instanceof ASN1.IA5String || fieldValue instanceof ASN1.Utf8String) {
101 123
      return fieldValue.valueBlock.value
102
    }
103
104 36
    return field.toJSON().valueBlock.valueHex
105
  }
106
107
  private addFieldToReceipt(name: ReceiptFieldsKeyNames, value: string): void {
108 159
    this.addToArrayFieldIfApplicable(name, value)
109 159
    this.parsed[name] = value
110
  }
111
112
  private addToArrayFieldIfApplicable(name: ReceiptFieldsKeyNames, value: string): void {
113 159
    const arrayFields: Record<string, keyof ParsedReceipt> = {
114
      'IN_APP_ORIGINAL_TRANSACTION_ID': 'IN_APP_ORIGINAL_TRANSACTION_IDS',
115
      'IN_APP_TRANSACTION_ID': 'IN_APP_TRANSACTION_IDS',
116
    }
117
118 159
    const arrayFieldName = arrayFields[name]
119 159
    if (arrayFieldName) {
120 30
      (this.parsed[arrayFieldName] as string[]).push(value)
121
    }
122
  }
123
124
  private validateParsedFields(): void {
125 3
    const missingFields = Array.from(RECEIPT_FIELDS_MAP.values())
126 51
      .filter(fieldKey => !(fieldKey in this.parsed))
127
128 3
    if (missingFields.length > 0) {
129
      throw new Error(`Missing required fields: ${missingFields.join(', ')}`)
130
    }
131
  }
132
133
  private deduplicateArrayFields(): void {
134 3
    this.parsed.IN_APP_ORIGINAL_TRANSACTION_IDS = this.removeDuplicates(this.parsed.IN_APP_ORIGINAL_TRANSACTION_IDS)
135 3
    this.parsed.IN_APP_TRANSACTION_IDS = this.removeDuplicates(this.parsed.IN_APP_TRANSACTION_IDS)
136
  }
137
138
  private removeDuplicates(array: string[]): string[] {
139 6
    return [...new Set(array)]
140
  }
141
}
142
143 1
export function parseReceipt(receipt: string): ParsedReceipt {
144 5
  return new ReceiptParser().parseReceipt(receipt)
145
}
146